// <copyright file="RPGEngine.cs" company="TenneySoftware">
// Copyright (c) 2009 All Right Reserved
// </copyright>
// <author>Robert Tenney</author>
// <email>rob10e@yahoo.com</email>
// <date>April 2009</date>
// <summary>The main engine which controls and provides the interation between all of the modules with the game scripts.</summary>

// License:

// Product: RPGRoguelikeEngine .Net
// Author: Robert N. Tenney
// Copyright: April 2009

// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation version 2
// of the License.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

// This license covers all source code, binaries, and other materials related to this project
// unless specifically stated otherwise.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Windows.Forms;

using TenneySoftware.Engine.Characters;
using TenneySoftware.Engine.Debug;
using TenneySoftware.Engine.GraphicsManager;
using TenneySoftware.LevelGenerator;

namespace TenneySoftware.Engine {
    /// <summary>
    /// The main engine which controls and provides the interation between all of the modules with the game scripts.
    /// </summary>
    public partial class RPGEngine : IDebugConsole {
        /// <summary>
        /// The loaded level generator.
        /// </summary>
        private ILevelGenerator levelGenerator;
        
        /// <summary>
        /// The loaded graphics manager.
        /// </summary>
        private IGraphicsManager graphicsManager;

        /// <summary>
        /// The level as it was last generated.
        /// </summary>
        private Level level;

        /// <summary>
        /// List of available level generator modules.
        /// </summary>
        private GeneratorDataList generatorLists;

        /// <summary>
        /// The current turn.
        /// </summary>
        private int turn;

        /// <summary>
        /// The debug console.
        /// </summary>
        private DebugConsole console;

        /// <summary>
        /// Are we in console mode.
        /// </summary>
        private bool consoleModeActive;

        /// <summary>
        /// List of all characters (player, npc, monsters) on the level as well as their location.
        /// </summary>
        private Dictionary<CharacterBase, Point> characterList;

        /// <summary>
        /// Debug modes processor.
        /// </summary>
        private DebugModeProcessor processor;

        /// <summary>
        /// A reference to the player.
        /// </summary>
        private Player player;
                
        /// <summary>
        /// Initializes a new instance of the RPGEngine class.
        /// </summary>
        public RPGEngine() {
            this.console = new DebugConsole(this);
            this.characterList = new Dictionary<CharacterBase, Point>();
            this.InitializeDebugProcessor();
            Actions.Engine = this;
            DebugConsole.Console = this.console;
        }
        
        /// <summary>
        /// Gets or sets the current number of turns played.
        /// </summary>
        /// <value>
        /// The current number of turns played.
        /// </value>
        public int Turn {
            get {
                return this.turn;
            }

            set {
                this.turn = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether or not console mode is active.
        /// </summary>
        /// <value>
        /// A value indicating whether or not console mode is active.
        /// </value>
        public bool ConsoleModeActive {
            get {
                return this.consoleModeActive;
            }

            set {
                this.consoleModeActive = value;
            }
        }

        /// <summary>
        /// Gets the debug console object to use with this instance.
        /// </summary>
        /// <value>
        /// The debug console object to use with this instance.
        /// </value>
        public DebugConsole Console {
            get {
                return this.console;
            }
        }

        /// <summary>
        /// Gets the level generator that generated the level.
        /// </summary>
        /// <value>
        /// The level generator that generated the level.
        /// </value>
        public ILevelGenerator LevelGenerator {
            get {
                return this.levelGenerator;
            }
        }

        /// <summary>
        /// Gets the graphics manager that is responsible for drawing the screen.
        /// </summary>
        /// <value>
        /// The graphics manager that is responsible for drawing the screen.
        /// </value>
        public IGraphicsManager GraphicsManager {
            get {
                return this.graphicsManager;
            }
        }

        /// <summary>
        /// Gets or sets the level.
        /// </summary>
        /// <value>
        /// The level.
        /// </value>
        public Level Level {
            get {
                return this.level;
            }

            set {
                this.level = value;
            }
        }

        /// <summary>
        /// Gets or sets the list of generators.
        /// </summary>
        /// <value>
        /// The list of generators.
        /// </value>
        public GeneratorDataList GeneratorLists {
            get {
                return this.generatorLists;
            }

            set {
                this.generatorLists = value;
            }
        }

        /// <summary>
        /// Gets the location of the player.
        /// </summary>
        /// <value>
        /// The location of the player.
        /// </value>
        public Point PlayerLocation {
            get {
                return this.characterList[this.player];
            }
        }

        /// <summary>
        /// Gets the list of characters with their locations.
        /// </summary>
        /// <value>
        /// The list of characters with their locations.
        /// </value>
        public Dictionary<CharacterBase, Point> CharacterList {
            get {
                return this.characterList;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the active tile is selectable or not.
        /// </summary>
        /// <value>
        /// A value indicating whether the active tile is selectable or not.
        /// </value>
        public bool Selectable {
            get {
                return this.Level.Bounds.Contains(this.GraphicsManager.ActiveTile) &&
                    ((this.GetTile(GraphicsManager.ActiveTile).HasSeen &&
                      this.GraphicsManager.DoFieldOfView) ||
                     !this.GraphicsManager.DoFieldOfView);
            }
        }
        
        /// <summary>
        /// Gets or sets the debug mode processor.
        /// </summary>
        /// <value>
        /// The debug mode processor.
        /// </value>
        protected DebugModeProcessor Processor {
            get {
                return this.processor;
            }
            
            set {
                this.processor = value;
            }
        }

        /// <summary>
        /// Given the string name of the generator, find and return the string array for that generator.
        /// </summary>
        /// <param name="generatorName">String name of generator.</param>
        /// <returns>String array of the generator.</returns>
        public static string[] FindGenerator(string generatorName) {
            string[][] temp = ListGenerators();
            foreach (string[] generate in temp) {
                if (generate != null) {
                    if (generate[0] == generatorName) {
                        return generate;
                    }
                }
            }

            throw new DllNotFoundException("Generator by the name of '" + generatorName + "' was not found.");
        }

        /// <summary>
        /// Given the string name of the manager, find and return the string array for that manager.
        /// </summary>
        /// <param name="managerName">String name of manager.</param>
        /// <returns>String array of the manager.</returns>
        public static string[] FindGraphicsManager(string managerName) {
            string[][] temp = ListGraphicsManagers();
            foreach (string[] manager in temp) {
                if (manager != null) {
                    if (manager[0] == managerName) {
                        return manager;
                    }
                }
            }

            throw new DllNotFoundException("Graphics manager by the name of '" + managerName + "' was not found.");
        }

        /// <summary>
        /// List the predefined settings that are associated with the given generator.
        /// </summary>
        /// <param name="generator">Generator string array.</param>
        /// <returns>Array of GeneratorData objects that decribe the predefined settings information.</returns>
        public static GeneratorData[] ListPredefines(string[] generator) {
            Assembly assem = Assembly.LoadFile(generator[1]);
            Type type = assem.GetType(generator[2]);
            ILevelGenerator temp = (ILevelGenerator)Activator.CreateInstance(type);

            return temp.GetPredefinedInformation();
        }

        /// <summary>
        /// Retrieve a jagged array of strings that list the generators with their assembly information.
        /// </summary>
        /// <returns>A jagged array of strings.</returns>
        public static string[][] ListGenerators() {
            string[][] results;
            string dir;
            dir = Application.ExecutablePath;
            string[] files = Directory.GetFiles(Path.Combine(Path.GetDirectoryName(dir), "LevelGenerators"), "*.dll");
            results = new string[files.Length][];
            int iter = 0;
            foreach (string file in files) {
                Assembly assembly = Assembly.LoadFile(file);
                foreach (Type type in assembly.GetTypes()) {
                    if (!type.IsClass || type.IsNotPublic) {
                        continue;
                    }

                    Type interfaces = type.GetInterface(typeof(ILevelGenerator).ToString());
                    if (interfaces != null) {
                        object obj = Activator.CreateInstance(type);
                        ILevelGenerator t = (ILevelGenerator)obj;
                        results[iter] = new string[] { t.ModuleName, file, type.ToString() };
                    }
                }

                iter++;
            }

            return results;
        }

        /// <summary>
        /// Retrieve a jagged array of strings that list the managers with their assembly information.
        /// </summary>
        /// <returns>A jagged array of strings.</returns>
        public static string[][] ListGraphicsManagers() {
            string[][] results;
            string dir;
            dir = Application.ExecutablePath;
            string[] files = Directory.GetFiles(Path.Combine(Path.GetDirectoryName(dir), "GraphicsManagers"), "*.dll");
            results = new string[files.Length][];
            int iter = 0;
            foreach (string file in files) {
                Assembly assembly = Assembly.LoadFile(file);
                foreach (Type type in assembly.GetTypes()) {
                    if (!type.IsClass || type.IsNotPublic) {
                        continue;
                    }

                    Type interfaces = type.GetInterface(typeof(IGraphicsManager).ToString());
                    if (interfaces != null) {
                        object obj = Activator.CreateInstance(type);
                        IGraphicsManager t = (IGraphicsManager)obj;
                        results[iter] = new string[] { t.ModuleName, file, type.ToString() };
                    }
                }

                iter++;
            }

            return results;
        }

        /// <summary>
        /// Increments the number of turns.
        /// </summary>
        public void IncreaseTurn() {
            this.Turn++;
        }

        /// <summary>
        /// Calls the generate method for the level generator module.
        /// </summary>
        /// <returns>True if level was generated.</returns>
        public bool GenerateLevel() {
            bool result = false;
            if (this.LevelGenerator != null) {
                result = this.LevelGenerator.Generate();
            }

            if (result) {
                this.Level = this.LevelGenerator.Level;
            }

            return result;
        }

        /// <summary>
        /// Loads the generator given.
        /// </summary>
        /// <param name="generator">The string array for the generator with the settings to load.</param>
        public void LoadGenerator(string[] generator) {
            Assembly assem = Assembly.LoadFile(generator[1]);
            Type type = assem.GetType(generator[2]);
            this.levelGenerator = (ILevelGenerator)Activator.CreateInstance(type);

            this.GeneratorLists = new GeneratorDataList();
            GeneratorData[] data = this.LevelGenerator.GetUniqueMethodInformation();
            try {
                this.GeneratorLists.Methods.AddRange(data);
            } catch (ArgumentNullException) {
                // Skip if it fails.
            }

            data = this.LevelGenerator.GetUniquePropertyInformation();
            try {
                this.GeneratorLists.Properties.AddRange(data);
            } catch (ArgumentNullException) {
                // Skip if it fails.
            }

            data = this.LevelGenerator.GetPredefinedInformation();
            this.GeneratorLists.Predefineds.AddRange(data);
        }

        /// <summary>
        /// Loads the manager given.
        /// </summary>
        /// <param name="manager">The string array for the manager with the settings to load.</param>
        public void LoadManager(string[] manager) {
            Assembly assem = Assembly.LoadFile(manager[1]);
            Type type = assem.GetType(manager[2]);
            this.graphicsManager = (IGraphicsManager)Activator.CreateInstance(type);
            this.GraphicsManager.Engine = this;
        }

        /// <summary>
        /// Causes an update to be performed for the graphics manager.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Lowercase is preferred.")]
        public void Update() {
            this.UpdateMessages();
            this.GraphicsManager.Update();
        }
        
        /// <summary>
        /// Given a string name for a predefined settings, find and return the integer code for that predefined.
        /// </summary>
        /// <param name="predefinedName">String name of predefined.</param>
        /// <returns>Integer code of predefined.</returns>
        public int FindPredefinedIndex(string predefinedName) {
            try {
                return this.GeneratorLists.Predefineds.Find(delegate(GeneratorData p) {
                                                                return p.Name == predefinedName;
                                                            }
                                                           ).Index;
            } catch (NullReferenceException) {
                throw new PredefineNameNotFoundException(
                    "Predefine by the name of '" + predefinedName + "' was not found in " +
                    this.LevelGenerator.ModuleName + " generator.",
                    this.LevelGenerator.ToString());
            }
        }

        /// <summary>
        /// Place a given character at a random point within the level.
        /// </summary>
        /// <param name="character">Character to place.</param>
        /// <returns>True if a character was successfully place, false otherwise.</returns>
        public bool PlaceCharacterAtStart(CharacterBase character) {
            return this.SetCharacter(character, this.SetStartingPoint());
        }

        /// <summary>
        /// Places a character on the level. The character is placed at location 0, 0. You need to use a placement
        /// call on this character before it can be used.
        /// </summary>
        /// <param name="character">Character to place on level.</param>
        /// <returns>True if character was successfully place, false otherwise.</returns>
        public bool SetCharacter(CharacterBase character) {
            return this.SetCharacter(character, Point.Empty);
        }

        /// <summary>
        /// Places a character on the level at a specific point. If a character is no currenly within the level, it will be
        /// placed on the level, if it was, then it will be moved from it's prior location to the new location.
        /// </summary>
        /// <param name="character">Character to set at location.</param>
        /// <param name="location">Location to set character at.</param>
        /// <returns>True if successful, false otherwise.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Object is tested before casting.")]
        public bool SetCharacter(CharacterBase character, Point location) {
            bool result = false;

            // Check to see if the desination is a valid place to occupy.
            if (!(this.IsLocationAWall(location) && !location.IsEmpty)) {
                // Check to see if the character is the player, if so, we need to update the player tracker.
                if (character is Player) {
                    this.player = character as Player;

                    // FIXME: Is this the right location for this?
                    if (this.console.Target == null) {
                        this.console.Target = this.player.Identification;
                    }
                }

                // Look up the character in the list.
                if (this.characterList.ContainsKey(character)) {
                    // Clear the tile from having the character.
                    this.Level.GetLevel(characterList[character]).Character = null;

                    // Update the character list with the new location.
                    this.characterList[character] = location;
                } else {
                    // If the character isn't in the list, then this is it's first appearance, so add it.
                    this.characterList.Add(character, location);
                }

                result = true;
                this.Level.GetLevel(location).Character = character;
                this.DoFieldOfViewCalculation();
            }
            
            return result;
        }

        /// <summary>
        /// Sets the tile data at the specified level location. The location is the tile's
        /// actual location, not the screen's relative location.
        /// </summary>
        /// <param name="tile">The tile to set at the location.</param>
        /// <param name="location">Location of tile to set the data for.</param>
        public void SetTile(TileData tile, Point location) {
            this.Level.SetLevel(location, tile);
            this.DoFieldOfViewCalculation();
        }

        /// <summary>
        /// Checks to see if a location be occupied by a character or object.
        /// </summary>
        /// <param name="location">Location to check.</param>
        /// <returns>True if location is okay of occupation.</returns>
        public bool CanTileBeOccupied(Point location) {
            bool result = false;
            if (this.Level.IsLocationWithinBounds(location)) {
                TileData tile = this.Level.GetLevel(location);
                if ((!tile.HasSeen && this.GraphicsManager.DoFieldOfView) || tile.TypeOfTile == TileType.Wall) {
                    result = false;
                } else {
                    result = true;
                }
            } else {
                result = false;
            }

            return result;
        }

        /// <summary>
        /// Converts the level data into a byte array with weights for the pathfinder.
        /// </summary>
        /// <returns>The byte array.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1814:PreferJaggedArraysOverMultidimensional", MessageId = "Return", Justification = "Multidimensional preferred.")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1814:PreferJaggedArraysOverMultidimensional", MessageId = "Body", Justification = "Multidimensional preferred.")]
        public byte[,] ConvertToPathfinder() {
            int convWidth = 0;
            int convHeight = 0;
            List<int> multiples = new List<int>(11);
            for (int iter = 2; iter < 2050; iter *= 2) {
                multiples.Add(iter);
            }

            // Cavern must be multiples of two for the pathfinder to work
            if (!multiples.Contains(this.Level.Width)) {
                int wid = this.Level.Width << 1;
                for (int iter = 2; iter < wid; iter *= 2) {
                    convWidth = iter;
                }
            } else {
                convWidth = this.Level.Width;
            }

            if (!multiples.Contains(this.Level.Height)) {
                int hei = this.Level.Height << 1;
                for (int iter = 2; iter < hei; iter *= 2) {
                    convHeight = iter;
                }
            } else {
                convHeight = this.Level.Height;
            }

            byte[,] temp = new byte[convWidth, convHeight];
            for (int y = this.Level.Bounds.Top; y < this.Level.Bounds.Bottom; y++) {
                for (int x = this.Level.Bounds.Left; x < this.Level.Bounds.Right; x++) {
                    TileData tile = this.Level.GetLevel(x, y);
                    if (x == 0 ||
                        x == this.Level.Width - 1 ||
                        y == 0 ||
                        y == this.Level.Height - 1) {
                        temp[x, y] = 0;
                    } else {
                        switch (tile.TypeOfTile) {
                            case TileType.Floor:
                                temp[x, y] = 1;
                                break;
                            case TileType.Door:
                                temp[x, y] = 10;
                                break;
                        }

                        if (tile.CanSee) {
                            temp[x, y] += 0;
                        } else {
                            temp[x, y] += 5;
                        }

                        if (!tile.HasSeen && this.GraphicsManager.DoFieldOfView) {
                            temp[x, y] = 0;
                        } else {
                            temp[x, y] += 0;
                        }

                        if (tile.TypeOfTile == TileType.Wall) {
                            temp[x, y] = 0;
                        }
                    }
                }
            }

            return temp;
        }

        /// <summary>
        /// Gets the tile data at the specified level location. The location is the tile's
        /// actual location, not the screen's relative location.
        /// </summary>
        /// <param name="location">Location of tile to retrieve the data for.</param>
        /// <returns>TileData at the specified location.</returns>
        public TileData GetTile(Point location) {
            return this.Level.GetLevel(location);
        }

        /// <summary>
        /// Processes the command string sent by the DebugConsole.
        /// </summary>
        /// <param name="command">Command string to process.</param>
        /// <returns>True if command was handled.</returns>
        public bool ConsoleCommand(string command) {
            return this.Processor.ConsoleCommand(command);
        }

        /// <summary>
        /// Generates a random point within the bounds of the level.
        /// </summary>
        /// <returns>A random point.</returns>
        private Point GenerateRandomPoint() {
            Random rand = new Random();
            Point temp = new Point();
            do {
                temp.X = rand.Next(0, this.Level.Width);
                temp.Y = rand.Next(0, this.Level.Height);
            } while (!this.Level.IsLocationWithinBounds(temp));
            return temp;
        }

        /// <summary>
        /// Finds a random point within the level that can be occupied. Used for debugging.
        /// </summary>
        /// <returns>A Point with the location found.</returns>
        private Point SetStartingPoint() {
            bool complete = false;
            Point result = Point.Empty;
            do {
                result = this.GenerateRandomPoint();
                if (!this.IsLocationAWall(result)) {
                    complete = true;
                }
            } while (!complete);
            return result;
        }

        /// <summary>
        /// Checks to see if the location has a wall or not.
        /// </summary>
        /// <param name="location">Location to check.</param>
        /// <returns>True if there is a wall at the point, otherwise false.</returns>
        private bool IsLocationAWall(Point location) {
            if (this.Level.IsLocationWithinBounds(location)) {
                TileData test = this.Level.GetLevel(location);
                if (test.TypeOfTile != TileType.Wall) {
                    return false;
                } else {
                    return true;
                }
            } else {
                return false;
            }
        }
        
        /// <summary>
        /// Update the dynamic messages.
        /// </summary>
        private void UpdateMessages() {
            DebugDynamicMessageCollection messages = new DebugDynamicMessageCollection();
            messages.Add(new DebugDynamicMessage("PlayerLocation", this.player.Name + " is at: " + this.PlayerLocation.ToString(), 1, Position.Left));
            messages.Add(new DebugDynamicMessage("Environment", "Environment: " + this.GetTile(this.PlayerLocation).TileDescription, 2, Position.Left));
            messages.Add(new DebugDynamicMessage("VisionRange", "Visual range: " + this.player.VisionRange, 2, Position.Left));
            messages.Add(new DebugDynamicMessage("Turns", "Current turn: " + this.Turn, 3, Position.Left));
            messages.Add(new DebugDynamicMessage("Generator", this.LevelGenerator.ModuleName + " - " + this.LevelGenerator.ModuleDescription, 4, Position.Left));
            messages.Add(new DebugDynamicMessage("Manager", this.GraphicsManager.ModuleName + " - " + this.GraphicsManager.ModuleDescription, 4, Position.Left));
            this.processor.UpdateMessages(messages);
        }

        /// <summary>
        /// Initializes the debug mode processor.
        /// </summary>
        private void InitializeDebugProcessor() {
            DebugMode[] modes = new DebugMode[] {
                new DebugMode("PlayerLocation", 1, "Displays the player's location."), 
                new DebugMode("Environment", 1, "Displays the type of environment that the player is in."), 
                new DebugMode("Turns", 1, "Displays the current turn."), 
                new DebugMode("VisionRange", 1, "Displays players vision range."), 
                new DebugMode("Generator", 1, "Displays the last used generator."),
                new DebugMode("Manager", 1, "Displays the current graphics manager.")
            };

            this.processor = new DebugModeProcessor(modes, "Engine");
        }

        /// <summary>
        /// Runs the FOV / FOW algorithm.
        /// </summary>
        private void DoFieldOfViewCalculation() {
            this.ClearCanSee();
            Point location = Point.Empty;
            int range = 0;
            foreach (KeyValuePair<CharacterBase, Point> kvp in this.characterList) {
                if (kvp.Key is Player) {
                    location = kvp.Value;
                    range = kvp.Key.VisionRange;
                }
            }

            FieldOfView.DoFieldOfViewCalculation(this.Level, location, range);
            this.GraphicsManager.Invalidate();
        }

        /// <summary>
        /// Clears all of the 'CanSee' flags for each tile. Part of the FOW feature.
        /// </summary>
        private void ClearCanSee() {
            for (int y = 0; y < this.Level.Size.Height; y++) {
                for (int x = 0; x < this.Level.Size.Width; x++) {
                    this.Level.GetLevel(x, y).CanSee = false;
                }
            }
        }
    }
}
